/*
 * Copyright 2014-2025 Real Logic Limited.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.agrona;

import org.agrona.concurrent.UnsafeBuffer;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.CsvSource;
import org.junit.jupiter.params.provider.MethodSource;
import org.junit.jupiter.params.provider.ValueSource;

import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.ThreadLocalRandom;
import java.util.function.Supplier;

import static java.nio.charset.StandardCharsets.US_ASCII;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verifyNoInteractions;

/**
 * Base class containing a common set of tests for {@link MutableDirectBuffer} implementations.
 */
public abstract class MutableDirectBufferTests
{
    private static final int ROUND_TRIP_ITERATIONS = 10_000_000;

    /**
     * Allocate new buffer with the specified capacity.
     *
     * @param capacity to allocate.
     * @return new buffer.
     */
    protected abstract MutableDirectBuffer newBuffer(int capacity);

    @Test
    void shouldThrowExceptionWhenOutOfBounds()
    {
        final MutableDirectBuffer buffer = newBuffer(10);
        final int index = buffer.capacity();
        assertThrows(IndexOutOfBoundsException.class, () -> buffer.getByte(index));
    }

    @Test
    void shouldPutBytes()
    {
        final int index = 5;
        final String value = "Hello World";
        final MutableDirectBuffer buffer = newBuffer(20);

        buffer.putBytes(index, value.getBytes(US_ASCII));

        assertEquals(value, buffer.getStringWithoutLengthAscii(index, value.length()));
    }

    @Test
    void shouldPutBytesFromByteBuffer()
    {
        final int destIndex = 2;
        final int srcIndex = 5;
        final String value = "Error converting parameter at index 0";
        final int payloadLength = value.length() - srcIndex;
        final ByteBuffer srcBuffer = ByteBuffer.allocate(value.length());
        srcBuffer.put(value.getBytes(US_ASCII), 0, value.length());
        srcBuffer.flip();
        final MutableDirectBuffer buffer = newBuffer(100);

        buffer.putBytes(destIndex, srcBuffer, srcIndex, payloadLength);

        assertEquals(
            value.substring(srcIndex), buffer.getStringWithoutLengthAscii(destIndex, payloadLength));
    }

    @ParameterizedTest
    @MethodSource("valuesAndLengths")
    void shouldPutNaturalFromEnd(final int value, final int length)
    {
        final MutableDirectBuffer buffer = newBuffer(8 * 1024);

        final int start = buffer.putNaturalIntAsciiFromEnd(value, length);
        final Supplier<String> messageSupplier = () -> "value=" + value + " length=" + length;
        assertEquals(0, start, messageSupplier);

        assertEquals(String.valueOf(value), buffer.getStringWithoutLengthAscii(0, length), messageSupplier);
    }

    @Test
    void putIntAsciiRoundTrip()
    {
        final int index = 4;
        final MutableDirectBuffer buffer = newBuffer(64);

        for (int i = 0; i < ROUND_TRIP_ITERATIONS; i++)
        {
            final int value = ThreadLocalRandom.current().nextInt();
            final int length = buffer.putIntAscii(index, value);
            final int parsedValue = buffer.parseIntAscii(index, length);
            assertEquals(value, parsedValue);
        }
    }

    @Test
    void putLongAsciiRoundTrip()
    {
        final int index = 16;
        final MutableDirectBuffer buffer = newBuffer(64);

        for (int i = 0; i < ROUND_TRIP_ITERATIONS; i++)
        {
            final long value = ThreadLocalRandom.current().nextLong();
            final int length = buffer.putLongAscii(index, value);
            final long parsedValue = buffer.parseLongAscii(index, length);
            assertEquals(value, parsedValue);
        }
    }

    @Test
    void putNaturalIntAsciiRoundTrip()
    {
        final int index = 8;
        final MutableDirectBuffer buffer = newBuffer(64);

        for (int i = 0; i < ROUND_TRIP_ITERATIONS; i++)
        {
            final int value = ThreadLocalRandom.current().nextInt(0, Integer.MAX_VALUE);
            final int length = buffer.putNaturalIntAscii(index, value);
            final int parsedValue = buffer.parseNaturalIntAscii(index, length);
            assertEquals(value, parsedValue);
        }
    }

    @Test
    void putNaturalLongAsciiRoundTrip()
    {
        final int index = 12;
        final MutableDirectBuffer buffer = newBuffer(64);

        for (int i = 0; i < ROUND_TRIP_ITERATIONS; i++)
        {
            final long value = ThreadLocalRandom.current().nextLong(0, Long.MAX_VALUE);
            final int length = buffer.putNaturalLongAscii(index, value);
            final long parsedValue = buffer.parseNaturalLongAscii(index, length);
            assertEquals(value, parsedValue);
        }
    }

    @ParameterizedTest
    @ValueSource(ints = { 11, 64, 1011 })
    void setMemory(final int length)
    {
        final int index = 2;
        final byte value = (byte)11;
        final MutableDirectBuffer buffer = newBuffer(2 * index + length);

        buffer.setMemory(index, length, value);

        assertEquals(0, buffer.getByte(0));
        assertEquals(0, buffer.getByte(1));
        assertEquals(0, buffer.getByte(index + length));
        assertEquals(0, buffer.getByte(index + length + 1));
        for (int i = 0; i < length; i++)
        {
            assertEquals(value, buffer.getByte(index + i));
        }
    }

    @Test
    void putLongAsciiShouldHandleEightDigitNumber()
    {
        final int index = 0;
        final MutableDirectBuffer buffer = newBuffer(16);

        final int length = buffer.putLongAscii(index, 87654321);
        assertEquals(8, length);

        assertEquals("87654321", buffer.getStringWithoutLengthAscii(index, length));
    }

    @ParameterizedTest
    @ValueSource(longs = { Long.MIN_VALUE, 0, Long.MAX_VALUE })
    void putLongAsciiShouldEncodeBoundaryValues(final long value)
    {
        final String encodedValue = Long.toString(value);
        final int index = 4;
        final MutableDirectBuffer buffer = newBuffer(32);

        final int length = buffer.putLongAscii(index, value);

        assertEquals(encodedValue.length(), length);
        assertEquals(encodedValue, buffer.getStringWithoutLengthAscii(index, length));
        assertEquals(value, buffer.parseLongAscii(index, length));
    }

    @ParameterizedTest
    @ValueSource(ints = { Integer.MIN_VALUE, 0, Integer.MAX_VALUE })
    void putIntAsciiShouldEncodeBoundaryValues(final int value)
    {
        final String encodedValue = Integer.toString(value);
        final int index = 3;
        final MutableDirectBuffer buffer = newBuffer(32);

        final int length = buffer.putIntAscii(index, value);

        assertEquals(encodedValue.length(), length);
        assertEquals(encodedValue, buffer.getStringWithoutLengthAscii(index, length));
        assertEquals(value, buffer.parseIntAscii(index, length));
    }

    @ParameterizedTest
    @MethodSource("nonParsableIntValues")
    void parseIntAsciiThrowsAsciiNumberFormatExceptionIfValueContainsInvalidCharacters(final String value)
    {
        final int index = 2;
        final MutableDirectBuffer buffer = newBuffer(16);
        final int length = buffer.putStringWithoutLengthAscii(index, value);

        final AsciiNumberFormatException exception =
            assertThrowsExactly(AsciiNumberFormatException.class, () -> buffer.parseIntAscii(index, length));
        assertEquals("error parsing int: " + value, exception.getMessage());
    }

    @ParameterizedTest
    @MethodSource("nonParsableLongValues")
    void parseLongAsciiThrowsAsciiNumberFormatExceptionIfValueContainsInvalidCharacters(
        final String value, final int baseIndex)
    {
        final int index = 7;
        final MutableDirectBuffer buffer = newBuffer(32);
        final int length = buffer.putStringWithoutLengthAscii(index, value);

        final AsciiNumberFormatException exception =
            assertThrowsExactly(AsciiNumberFormatException.class, () -> buffer.parseLongAscii(index, length));
        assertEquals("error parsing long: " + value, exception.getMessage());
    }

    @ParameterizedTest
    @MethodSource("nonParsableIntValues")
    void parseNaturalIntAsciiThrowsAsciiNumberFormatExceptionIfValueContainsInvalidCharacters(final String value)
    {
        final int index = 1;
        final MutableDirectBuffer buffer = newBuffer(16);
        final int length = buffer.putStringWithoutLengthAscii(index, value);

        final AsciiNumberFormatException exception =
            assertThrowsExactly(AsciiNumberFormatException.class, () -> buffer.parseNaturalIntAscii(index, length));
        assertEquals("error parsing int: " + value, exception.getMessage());
    }

    @ParameterizedTest
    @MethodSource("nonParsableLongValues")
    void parseNaturalLongAsciiThrowsAsciiNumberFormatExceptionIfValueContainsInvalidCharacters(
        final String value, final int baseIndex)
    {
        final int index = 8;
        final MutableDirectBuffer buffer = newBuffer(32);
        final int length = buffer.putStringWithoutLengthAscii(index, value);

        final AsciiNumberFormatException exception =
            assertThrowsExactly(AsciiNumberFormatException.class, () -> buffer.parseNaturalLongAscii(index, length));
        assertEquals("error parsing long: " + value, exception.getMessage());
    }

    @ParameterizedTest
    @ValueSource(strings = {
        "-5547483650",
        "-2147483649",
        "2147483648",
        "2147483649",
        "9999999991",
        "-999999999999999999",
        "12345678901234567890" })
    void parseIntAsciiShouldThrowAsciiNumberFormatExceptionIfValueIsOutOfRange(final String value)
    {
        final int index = 4;
        final MutableDirectBuffer buffer = newBuffer(32);

        final int length = buffer.putStringWithoutLengthAscii(index, value);

        final AsciiNumberFormatException exception =
            assertThrowsExactly(AsciiNumberFormatException.class, () -> buffer.parseIntAscii(index, length));
        assertEquals("int overflow parsing: " + value, exception.getMessage());
    }

    @ParameterizedTest
    @ValueSource(strings = {
        "-9223372036854775810",
        "-9223372036854775809",
        "9223372036854775808",
        "9223372036854775809",
        "-19191919191919191919",
        "123456789012345678901234567890" })
    void parseLongAsciiShouldThrowAsciiNumberFormatExceptionIfValueIsOutOfRange(final String value)
    {
        final int index = 7;
        final MutableDirectBuffer buffer = newBuffer(64);

        final int length = buffer.putStringWithoutLengthAscii(index, value);

        final AsciiNumberFormatException exception =
            assertThrowsExactly(AsciiNumberFormatException.class, () -> buffer.parseLongAscii(index, length));
        assertEquals("long overflow parsing: " + value, exception.getMessage());
    }

    @ParameterizedTest
    @ValueSource(strings = {
        "2147483648",
        "2147483649",
        "9999999991",
        "12345678901234567890" })
    void parseNaturalIntAsciiShouldThrowAsciiNumberFormatExceptionIfValueIsOutOfRange(final String value)
    {
        final int index = 4;
        final MutableDirectBuffer buffer = newBuffer(32);

        final int length = buffer.putStringWithoutLengthAscii(index, value);

        final AsciiNumberFormatException exception =
            assertThrowsExactly(AsciiNumberFormatException.class, () -> buffer.parseNaturalIntAscii(index, length));
        assertEquals("int overflow parsing: " + value, exception.getMessage());
    }

    @ParameterizedTest
    @ValueSource(strings = {
        "9223372036854775808",
        "9223372036854775809",
        "123456789012345678901234567890" })
    void parseNaturalLongAsciiShouldThrowAsciiNumberFormatExceptionIfValueIsOutOfRange(final String value)
    {
        final int index = 7;
        final MutableDirectBuffer buffer = newBuffer(64);

        final int length = buffer.putStringWithoutLengthAscii(index, value);

        final AsciiNumberFormatException exception =
            assertThrowsExactly(AsciiNumberFormatException.class, () -> buffer.parseNaturalLongAscii(index, length));
        assertEquals("long overflow parsing: " + value, exception.getMessage());
    }

    @ParameterizedTest
    @MethodSource("mutableBuffers")
    void getBytesShouldCopyIntoDestinationBuffer(final MutableDirectBuffer dest)
    {
        final int srcIndex = 20;
        final int dstIndex = 13;
        final int length = 49;
        final byte[] data = new byte[64];
        ThreadLocalRandom.current().nextBytes(data);
        final MutableDirectBuffer buffer = newBuffer(100);
        buffer.putBytes(srcIndex, data);

        buffer.getBytes(srcIndex, dest, dstIndex, length);

        for (int i = 0; i < length; i++)
        {
            assertEquals(data[i], dest.getByte(dstIndex + i));
        }
    }

    @ParameterizedTest
    @MethodSource("mutableBuffers")
    void putBytesShouldCopyBytesFromTheSourceBuffer(final MutableDirectBuffer src)
    {
        final int srcIndex = 13;
        final int dstIndex = 6;
        final int length = 28;
        final byte[] data = new byte[32];
        ThreadLocalRandom.current().nextBytes(data);
        src.putBytes(srcIndex, data);

        final MutableDirectBuffer buffer = newBuffer(100);
        buffer.putBytes(dstIndex, src, srcIndex, length);

        for (int i = 0; i < length; i++)
        {
            assertEquals(data[i], buffer.getByte(dstIndex + i));
        }
    }

    @Test
    void equalsReturnsTrueForThis()
    {
        final MutableDirectBuffer buffer = newBuffer(1);
        assertEquals(buffer, buffer);
    }

    @Test
    void equalsReturnsFalseForNull()
    {
        final MutableDirectBuffer buffer = newBuffer(1);
        assertNotEquals(null, buffer);
    }

    @Test
    void equalsReturnsFalseForWrongClass()
    {
        final MutableDirectBuffer buffer = newBuffer(1);
        final AbstractMutableDirectBuffer other = mock(AbstractMutableDirectBuffer.class);

        assertNotEquals(buffer, other);

        verifyNoInteractions(other);
    }

    @Test
    void equalsReturnsFalseWhenCapacityDoesNotMatch()
    {
        final MutableDirectBuffer buffer = newBuffer(1);
        final MutableDirectBuffer other = newBuffer(3);

        assertNotEquals(buffer, other);
    }

    @ParameterizedTest
    @ValueSource(ints = { 1, 7, 21, 64 })
    void equalsReturnsTrueWhenOtherBufferHasTheSameData(final int capacity)
    {
        final byte[] data = new byte[capacity];
        ThreadLocalRandom.current().nextBytes(data);
        final MutableDirectBuffer buffer = newBuffer(capacity);
        final MutableDirectBuffer other = newBuffer(capacity);
        buffer.putBytes(0, data);
        other.putBytes(0, data);

        assertEquals(buffer, other);
    }

    @Test
    void hashCodeConsidersAnEntireCapacity()
    {
        final MutableDirectBuffer buffer1 = newBuffer(10);
        final MutableDirectBuffer buffer2 = newBuffer(15);
        final MutableDirectBuffer buffer3 = newBuffer(10);
        final int length = 7;
        buffer1.setMemory(0, length, (byte)'f');
        buffer2.setMemory(0, length, (byte)'f');
        buffer3.setMemory(0, length, (byte)'f');

        assertNotEquals(buffer1.hashCode(), buffer2.hashCode());
        assertEquals(buffer1.hashCode(), buffer3.hashCode());
    }

    @ParameterizedTest
    @CsvSource({ "3,687175", "11,17668045", "128,-56919533" })
    void hashCodeDifferentDataSizes(final int capacity, final int expectedHashCode)
    {
        final MutableDirectBuffer buffer = newBuffer(capacity);
        buffer.setMemory(0, capacity, (byte)'z');
        assertEquals(expectedHashCode, buffer.hashCode());
    }

    @Test
    void compareToComparesCapacityIfContentIsEqual()
    {
        final MutableDirectBuffer buffer1 = newBuffer(3);
        final MutableDirectBuffer buffer2 = newBuffer(5);
        final MutableDirectBuffer buffer3 = newBuffer(3);

        assertEquals(-1, buffer1.compareTo(buffer2));
        assertEquals(1, buffer2.compareTo(buffer1));
        assertEquals(0, buffer3.compareTo(buffer1));
    }

    @ParameterizedTest
    @ValueSource(ints = { 1, 6, 19, 256 })
    void compareToReturnsZeroForEqualContents(final int capacity)
    {
        final byte[] data = new byte[capacity];
        ThreadLocalRandom.current().nextBytes(data);
        final MutableDirectBuffer buffer1 = newBuffer(capacity);
        buffer1.putBytes(0, data);
        final MutableDirectBuffer buffer2 = new UnsafeBuffer(data);

        assertEquals(0, buffer1.compareTo(buffer2));
        assertEquals(0, buffer2.compareTo(buffer1));
    }

    @ParameterizedTest
    @ValueSource(ints = { 5, 33 })
    void compareToStopsComparisonUponTheFirstNonEqualByte(final int capacity)
    {
        final MutableDirectBuffer buffer1 = newBuffer(capacity);
        final MutableDirectBuffer buffer2 = newBuffer(capacity);
        final int index1 = ThreadLocalRandom.current().nextInt(capacity - 2, capacity);
        final int index2 = ThreadLocalRandom.current().nextInt(0, index1);
        buffer1.setMemory(0, index1, (byte)'a');
        buffer2.setMemory(0, index2, (byte)'a');
        buffer2.setMemory(index2, capacity - index2, (byte)'x');

        assertTrue(buffer1.compareTo(buffer2) < 0);
        assertTrue(buffer2.compareTo(buffer1) > 0);
    }

    @ParameterizedTest
    @ValueSource(ints = { 1, 5, 9, 16, 33, 100, 256 })
    void setMemoryCopiesTheSameByteAcross(final int length)
    {
        final int index = ThreadLocalRandom.current().nextInt(0, 10);
        final MutableDirectBuffer buffer = newBuffer(index + length);
        final byte value = (byte)ThreadLocalRandom.current().nextInt();
        final byte[] expected = new byte[length];
        Arrays.fill(expected, value);

        buffer.setMemory(index, length, value);

        final byte[] actual = new byte[length];
        buffer.getBytes(index, actual);
        assertArrayEquals(expected, actual);
    }

    private static List<Arguments> valuesAndLengths()
    {
        return Arrays.asList(
            Arguments.arguments(1, 1),
            Arguments.arguments(10, 2),
            Arguments.arguments(100, 3),
            Arguments.arguments(1000, 4),
            Arguments.arguments(12, 2),
            Arguments.arguments(123, 3),
            Arguments.arguments(2345, 4),
            Arguments.arguments(9, 1),
            Arguments.arguments(99, 2),
            Arguments.arguments(999, 3),
            Arguments.arguments(9999, 4));
    }

    private static List<String> nonParsableIntValues()
    {
        return Arrays.asList(
            "23.5",
            "+1",
            "a14349",
            "0xFF",
            "999v",
            "-",
            "+",
            "1234%67890");
    }

    private static List<Arguments> nonParsableLongValues()
    {
        return Arrays.asList(
            Arguments.arguments("23.5", 2),
            Arguments.arguments("+1", 0),
            Arguments.arguments("a14349", 0),
            Arguments.arguments("0xFF", 1),
            Arguments.arguments("999v", 3),
            Arguments.arguments("-", 0),
            Arguments.arguments("+", 0),
            Arguments.arguments("123456789^123456789", 9)
        );
    }

    private static List<MutableDirectBuffer> mutableBuffers()
    {
        return Arrays.asList(
            new ExpandableArrayBuffer(64),
            new ExpandableDirectByteBuffer(64),
            new UnsafeBuffer(new byte[64]),
            new UnsafeBuffer(ByteBuffer.allocate(64)),
            new UnsafeBuffer(ByteBuffer.allocateDirect(64)));
    }
}
